﻿using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
using TelnetExpect;

namespace TestServer1
{
    class Program
    {
        static void Main(string[] args)
        {
            new Program().Run();
        }

        //----
        const string Prompt = "> ";
        const string TerminalNewLine = "\r\n";
        const int PasscodeRandomSize = 4;
        //
        RandomNumberGenerator _rand = new RNGCryptoServiceProvider();

        public void Run()
        {
            var lsnr = new TcpListener(
                IPAddress.Loopback,
                999);
            lsnr.Start();
            Console.WriteLine("Listening...");
            //
            var stopServer = false;
            while (!stopServer) {
                var conn = lsnr.AcceptTcpClient();
                Console.WriteLine("Got connection from: " + conn.Client.RemoteEndPoint);
                var ts = new SimpleServerTelnetStream(conn.GetStream());
                //
                var somewhatDangerousDirectToCmdDotExe = false;
                if (somewhatDangerousDirectToCmdDotExe) {
                    ThreadPool.QueueUserWorkItem(_ => DirectToCmdDangerSecurity(ts, conn.Client.RemoteEndPoint));
                } else {
                    // Could be new thread too. (Without 'stopServer')...
                    ManualInterpreter(ts, ref stopServer);
                }
            }//while
        }

        //----
        private void ManualInterpreter(TelnetStream ts, ref bool stopServer)
        {
            var enc = Encoding.Default;
            var rdr = new StreamReader(ts, enc);
            using (var wtr = new StreamWriter(ts, enc)) {
                wtr.NewLine = TerminalNewLine;
                //
                wtr.WriteLine("Current commands are: ");
                wtr.WriteLine("   exit, exitserver, dir2, netstat.");
                //
                var stopSession = false;
                while (!stopSession) {
                    wtr.Write(Prompt);
                    wtr.Flush();
                    var line = rdr.ReadLine();
                    line = ApplyBackspaces(line);
                    var splits = line.Split(' ');
                    if (splits.Length == 0) {
                        // Just in case.  But we never get this (there's 
                        // always one empty entry in the splits array).
                    } else {
                        var cmd = splits[0];
                        var rest = line.Substring(cmd.Length);
                        rest = rest.TrimStart();
                        switch (cmd) {
                            case "exitserver":
                                stopServer = true;
                                wtr.WriteLine("[Server exiting]");
                                wtr.Flush();
                                goto case "exit";
                            case "quit":
                            case "exit":
                                stopSession = true;
                                break;
                            //
                            case "dir2":
                                // Manually handled
                                wtr.Write(Dir());
                                wtr.WriteLine();
                                break;
                            case "netstat":
                                // Exec'd
                                BasicExec(wtr, cmd, rest);
                                wtr.WriteLine();
                                break;
                            //
                            default:
                                wtr.Write("Unknown command: ");
                                wtr.Write(line);
                                wtr.WriteLine();
                                break;
                        }
                    }
                }//while
                wtr.WriteLine("[goodbye :-) ]");
                wtr.Flush();
            }//using
        }

        //----
        private void DirectToCmdDangerSecurity(SimpleServerTelnetStream ts, object connectionId)
        {
            CR2W stdInpRunner = CopyReaderToWriter;
            CR2W stdOutRunner = CopyReaderToWriter;
            CR2W stdErrRunner = CopyReaderToWriter;
            IAsyncResult arI = null, arO = null, arE = null;
            //
            var enc = Encoding.Default;
            var rdr = new StreamReader(ts, enc);
            try {
                using (var wtr = new StreamWriter(ts, enc)) {
                    wtr.NewLine = TerminalNewLine;
                    //
                    // User must copy passcode from console into terminal.
                    var expectedPasscode = CreateNewPasscode();
                    Console.WriteLine("** Passcode for session {0} is: {1}", connectionId, expectedPasscode);
                    wtr.Write("Enter passcode> ");
                    wtr.Flush();
                    var line = rdr.ReadLine();
                    line = line.Trim().ToLowerInvariant();
                    if (line != expectedPasscode) {
                        wtr.WriteLine("Wrong passcode!!!");
                        return;
                    }
                    //
                    try {
                        using (var p = new Process()) {
                            var psi = new ProcessStartInfo("cmd.exe");
                            //
                            psi.UseShellExecute = false;
                            psi.RedirectStandardInput = true;
                            psi.RedirectStandardOutput = true;
                            psi.RedirectStandardError = true;
                            //psi.CreateNoWindow = true;
                            //psi.ErrorDialog = false;
                            p.StartInfo = psi;
                            p.Start();
                            //
                            var safeWtr = TextWriter.Synchronized(wtr);
                            arI = stdInpRunner.BeginInvoke(rdr, p.StandardInput, "stdIn", null, null);
                            arO = stdOutRunner.BeginInvoke(p.StandardOutput, safeWtr, "stdOut", null, null);
                            arE = stdErrRunner.BeginInvoke(p.StandardError, safeWtr, "stdErr", null, null);
                            //
                            WaitHandle.WaitAny(new WaitHandle[] { arI.AsyncWaitHandle, arO.AsyncWaitHandle, arE.AsyncWaitHandle });
                        }
                    } catch (Exception ex) {
                        wtr.Write("Exception: " + ex.Message);
                    }
                }//using
            } finally {
#if DEBUG
                if (arI != null && arO != null && arE != null) {
                    var signalled0 = WaitHandle.WaitAll(new WaitHandle[] { arI.AsyncWaitHandle, arO.AsyncWaitHandle, arE.AsyncWaitHandle },
                        0);
                    var signalled2 = WaitHandle.WaitAll(new WaitHandle[] { arI.AsyncWaitHandle, arO.AsyncWaitHandle, arE.AsyncWaitHandle },
                        2 * 1000);
                    var signalled10 = WaitHandle.WaitAll(new WaitHandle[] { arI.AsyncWaitHandle, arO.AsyncWaitHandle, arE.AsyncWaitHandle },
                        10 * 1000);
                    var diag = "All streams done after 0/2/10: " + signalled0 + "/" + signalled2 + "/" + signalled10;
                    Debug.WriteLine(diag);
                    Console.WriteLine(diag);
                }
#endif
                if (arI != null && arO != null && arE != null) {
                    try {
                        stdInpRunner.EndInvoke(arI);
                    } catch (IOException) {
                    } catch (ObjectDisposedException) {
                    }
                    try {
                        stdOutRunner.EndInvoke(arO);
                    } catch (IOException) {
                    } catch (ObjectDisposedException) {
                    }
                    try {
                        stdErrRunner.EndInvoke(arE);
                    } catch (IOException) {
                    } catch (ObjectDisposedException) {
                    }
                }
                Console.WriteLine("exit");
            }
        }

        //----
        private string CreateNewPasscode()
        {
            byte[] randA4 = new byte[PasscodeRandomSize];
            _rand.GetBytes(randA4);
            var bldr = new StringBuilder();
            Array.ForEach(randA4, x => bldr.AppendFormat("{0:X2}", x));
            var expectedPasscode = bldr.ToString();
            expectedPasscode = expectedPasscode.ToLowerInvariant();
            return expectedPasscode;
        }

        //----
        delegate void CR2W(TextReader src, TextWriter wtr, string name);

        private static void CopyReaderToWriter(TextReader src, TextWriter wtr, string name)
        {
            var buf = new char[64 * 1024];
            while (true) {
                var len = src.Read(buf, 0, buf.Length);
                wtr.Write(buf, 0, len);
                wtr.Flush();
                if (len == 0) {
                    break;
                }
            }//while
            Debug.WriteLine("EoF on: " + name);
        }

        //----
        private string ApplyBackspaces(string line)
        {
            const char Backspace = '\b';
            if (!line.Contains(Backspace))
                return line;
            var bldr = new StringBuilder();
            foreach (var cur in line) {
                if (cur == Backspace) {
                    if (bldr.Length > 0) { bldr.Length -= 1; }
                } else {
                    bldr.Append(cur);
                }
            }//for
            return bldr.ToString();
        }

        private void PrintOptionHandlers(TelnetStream ts)
        {
            var bldr = new StringBuilder();
            var olist = ts.GetOptionHandlers();
            const string Sep = ", ";
            foreach (var cur in olist) {
                bldr.Append(cur.GetType().Name);
                bldr.Append(Sep);
            }//for
            if (bldr.Length > 0) bldr.Length -= Sep.Length;
            var s = bldr.ToString();
            if (s.Length == 0) s = "<none>";
            Console.WriteLine("olist: " + s);
        }

        //--------
        private void BasicExec(TextWriter wtr, string fileName, string args)
        {
            try {
                using (var p = new Process()) {
                    var psi = new ProcessStartInfo(fileName);
                    psi.Arguments = args;
                    //
                    psi.UseShellExecute = false;
                    psi.RedirectStandardInput = true;
                    psi.RedirectStandardOutput = true;
                    psi.RedirectStandardError = false; // TO-DO true;
                    //psi.CreateNoWindow = true;
                    //psi.ErrorDialog = false;
                    p.StartInfo = psi;
                    p.Start();
                    p.StandardInput.Close();
                    var buf = new char[64 * 1024];
                    while (true) {
                        var len = p.StandardOutput.Read(buf, 0, buf.Length);
                        wtr.Write(buf, 0, len);
                        wtr.Flush();
                        if (len == 0) {
                            break;
                        }
                    }//while
                }
            } catch (Exception ex) {
                wtr.Write("Exception: " + ex.Message);
            }
        }

        string Dir()
        {
            var bldr = new StringBuilder();
            var files = Directory.GetFiles(".");
            int numFiles = 0;// numDirs
            UInt64 totalLen = 0;
            foreach (var cur in files) {
                var f = new FileInfo(cur);
                bldr.AppendFormat("{0}  {1,15:N0} {2}",
                    f.LastWriteTime, f.Length, f.Name);
                bldr.Append(TerminalNewLine);
                //
                ++numFiles;
                unchecked { totalLen = totalLen + (UInt64)f.Length; }
            }//for
            bldr.AppendFormat("{0,16} File(s) {1,15:N0} bytes", numFiles, totalLen);
            bldr.Append(TerminalNewLine);
            return bldr.ToString();
        }
    }
}
